package org.skatiger.jrove.spring.security;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.skatiger.jrove.JroveException;
import org.skatiger.jrove.component.UIComponentBase;
import org.skatiger.jrove.spring.utils.JroveApplicationContextUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.security.acls.domain.DefaultPermissionFactory;
import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl;
import org.springframework.security.acls.domain.PermissionFactory;
import org.springframework.security.acls.domain.SidRetrievalStrategyImpl;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.ObjectIdentityRetrievalStrategy;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.acls.model.SidRetrievalStrategy;
import org.springframework.security.core.context.SecurityContextHolder;

public class AccessControlList extends UIComponentBase {
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory
            .getLogger(AccessControlList.class);
    private AclService aclService;
    private ApplicationContext applicationContext;
    private Object domainObject;
    private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy;
    private SidRetrievalStrategy sidRetrievalStrategy;
    private PermissionFactory permissionFactory;
    private String hasPermission;

    @Override
    public boolean isRendered() {
        return super.isRendered() ? _hasPermission() : false;
    }

    public boolean _hasPermission() {
        if ((null == hasPermission) || "".equals(hasPermission)) {
            return false;
        }

        initializeIfRequired();

        final String evaledPermissionsString = getHasPermission();

        List<Permission> requiredPermissions = parsePermissionsString(evaledPermissionsString);

        Object resolvedDomainObject = getDomainObject();

        if (resolvedDomainObject == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("domainObject resolved to null, so including tag body");
            }

            // Of course they have access to a null object!
            return true;
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("SecurityContextHolder did not return a non-null Authentication object, so skipping tag body");
            }

            return false;
        }

        List<Sid> sids = sidRetrievalStrategy.getSids(SecurityContextHolder
                .getContext().getAuthentication());
        ObjectIdentity oid = objectIdentityRetrievalStrategy
                .getObjectIdentity(resolvedDomainObject);

        // Obtain aclEntrys applying to the current Authentication object
        try {
            Acl acl = aclService.readAclById(oid, sids);

            if (acl.isGranted(requiredPermissions, sids, false)) {
                return true;
            } else {
                return false;
            }
        } catch (NotFoundException nfe) {
            return false;
        }
    }

    /**
     * Allows test cases to override where application context obtained from.
     *
     * @param pageContext so the <code>ServletContext</code> can be accessed as required by Spring's
     *        <code>WebApplicationContextUtils</code>
     *
     * @return the Spring application context (never <code>null</code>)
     */
    protected ApplicationContext getContext() {
        return JroveApplicationContextUtil
                .findApplicationContext(getViewContext());
    }

    public Object getDomainObject() {
        return getPropertyValue("domainObject", domainObject);
    }

    public String getHasPermission() {
        return getPropertyValue("hasPermission", hasPermission);
    }

    private void initializeIfRequired() {
        if (applicationContext != null) {
            return;
        }

        this.applicationContext = getContext();

        aclService = getBeanOfType(AclService.class);

        sidRetrievalStrategy = getBeanOfType(SidRetrievalStrategy.class);

        if (sidRetrievalStrategy == null) {
            sidRetrievalStrategy = new SidRetrievalStrategyImpl();
        }

        objectIdentityRetrievalStrategy = getBeanOfType(ObjectIdentityRetrievalStrategy.class);

        if (objectIdentityRetrievalStrategy == null) {
            objectIdentityRetrievalStrategy = new ObjectIdentityRetrievalStrategyImpl();
        }

        permissionFactory = getBeanOfType(PermissionFactory.class);

        if (permissionFactory == null) {
            permissionFactory = new DefaultPermissionFactory();
        }
    }

    private <T> T getBeanOfType(Class<T> type) {
        Map<String, T> map = applicationContext.getBeansOfType(type);

        for (ApplicationContext context = applicationContext.getParent(); context != null; context = context
                .getParent()) {
            map.putAll(context.getBeansOfType(type));
        }

        if (map.size() == 0) {
            return null;
        } else if (map.size() == 1) {
            return map.values().iterator().next();
        }

        throw new JroveException("Found incorrect number of "
                + type.getSimpleName() + " instances in "
                + "application context - you must have only have one!");
    }

    private List<Permission> parsePermissionsString(String permissionsString)
            throws NumberFormatException {
        final Set<Permission> permissions = new HashSet<Permission>();
        final StringTokenizer tokenizer;
        tokenizer = new StringTokenizer(permissionsString, ",", false);

        while (tokenizer.hasMoreTokens()) {
            String permission = tokenizer.nextToken();
            try {
                permissions.add(permissionFactory.buildFromMask(Integer
                        .valueOf(permission)));
            } catch (NumberFormatException nfe) {
                // Not an integer mask. Try using a name
                permissions.add(permissionFactory.buildFromName(permission));
            }
        }

        return new ArrayList<Permission>(permissions);
    }

    public void setDomainObject(Object domainObject) {
        this.domainObject = domainObject;
    }

    public void setHasPermission(String hasPermission) {
        this.hasPermission = hasPermission;
    }

}
